package nachos.network;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import nachos.machine.Kernel;
import nachos.machine.Lib;
import nachos.machine.Machine;
import nachos.machine.MalformedPacketException;
import nachos.machine.OpenFile;
import nachos.threads.KThread;
import nachos.threads.Lock;
import nachos.threads.Semaphore;

public class Socket extends OpenFile {
	/**
	 * Constants for connection state:
	 */
	public static final int CLOSED = 0;
	public static final int SYN_SENT = 1;
	public static final int SYN_RCVD = 2;
	public static final int ESTABLISHED = 3;
	public static final int STP_RCVD = 4;
	public static final int STP_SENT = 5;
	public static final int CLOSING = 6;
	private static final char dbgNet = 'n';
	public static final int maxReceiveBufferSize = 16;
	public static final int windowSize = 16;
		
	 /** The port used by this socket on the destination machine. */
    public int dstPort;
    /** The port used by this socket on the source machine. */
    public int srcPort;
    /** The link id of the remote host */
    public int dstHostID;
    public int srcHostID = Machine.networkLink().getLinkAddress();
    
    public Lock sendBufferLock;
    protected Lock receiveBufferLock;
    protected Lock nextSeqNoLock;
    
    public List<NachosPacket> sendBuffer;
    protected List<NachosPacket> receiveBuffer;
    
    /**
     * A readBuffer is an array where the bytes from the received packets
     * are buffered for reading. We have to do this, because a read()
     * may request only fraction of data that is in a packet. The rest of that
     * packet will be stored here
     */
    protected byte[] readBuffer;
    /** 
     * The next byte to read
     */
    protected int readBufferHead; 
    /**
     * The last byte to read
     */
    protected int readBufferTail;
     
    protected NachosProtocol protocol;
    
    protected int state = CLOSED;
    
    protected int nextSeqNo = -1;
    
    /**
     * This semaphore is used for blocking on connect()  call:
     */
    public Semaphore waitForConnSignal;
    
    private int nextToReceive = 0;
    
        
    /**
     * The constructor
     * @param dstHost
     * @param dstPort
     * @param srcPort
     * @param protocol
     */
    
    public Socket(int dstHost, int dstPort, int srcPort){
    	this.srcPort = srcPort;
    	this.dstHostID = dstHost;
    	this.dstPort = dstPort;
    	sendBuffer = new ArrayList<NachosPacket>();
    	receiveBuffer = new ArrayList<NachosPacket>();
    	sendBufferLock = new Lock();
    	receiveBufferLock = new Lock();
    	nextSeqNoLock = new Lock();
    	this.protocol = ((NetKernel)Kernel.kernel).protocol;
    	//allocate the byte array to hold the remainder of the last packet
    	// read. The packet can have up to 20 bytes of data (maxContentsLength)
    	// if the read requests only 10 for instance, we have to keep the 
    	// remaining 10 somwhere
    	this.readBuffer = new byte[NachosPacket.maxContentsLength];
    	this.readBufferHead = -1;
    	this.readBufferTail = -1;
    	this.waitForConnSignal = new Semaphore(0);
    }
    
    public Socket(int srcPort){
    	this.srcPort = srcPort;
    	this.dstHostID = 0;
    	this.dstPort = 0;
    	sendBuffer = new ArrayList<NachosPacket>();
    	receiveBuffer = new ArrayList<NachosPacket>();
    	sendBufferLock = new Lock();
    	receiveBufferLock = new Lock();
    	nextSeqNoLock = new Lock();
    	this.protocol = ((NetKernel)Kernel.kernel).protocol;
    	//allocate the byte array to hold the remainder of the last packet
    	// read. The packet can have up to 20 bytes of data (maxContentsLength)
    	// if the read requests only 10 for instance, we have to keep the 
    	// remaining 10 somwhere
    	this.readBuffer = new byte[NachosPacket.maxContentsLength];
    	this.readBufferHead = -1;
    	this.readBufferTail = -1;
    	this.waitForConnSignal = new Semaphore(0);
    }
    
    public int read(byte[] buffer, int offset, int length ){
    	// check for connection state before reading
    	KThread.currentThread().yield();
    	if(this.state !=ESTABLISHED && this.state != STP_RCVD)
    		return -1;
    	int bytesRead = 0;
    	//first, copy the bytes from the readBuffer if there are any:
    	int buffLen = readBufferTail - readBufferHead;  // some bytes might have been
    											//read already. 
    	if(readBufferHead > -1){
    		//if there are more or equal bytes in the readBuffer than we need, then 
    		//reset the length
    		if(length < buffLen){
    			buffLen = length;
       		}
    		System.arraycopy(readBuffer, readBufferHead, 
    						buffer, offset, buffLen);
    		
    		bytesRead = buffLen;
    		readBufferHead = readBufferHead + buffLen;
    		if(readBufferHead == readBuffer.length){
    			readBufferHead = -1;
    			readBufferTail = -1;
    		}
    		offset = offset + bytesRead;
    		length = length - bytesRead; 
    	}
    	receiveBufferLock.acquire();
    	while(receiveBuffer.size() > 0 && length > 0 ){
    		//read in all the packets we can read whole first
    		NachosPacket packet = receiveBuffer.remove(0);
    		Lib.debug('n',"Got packet from receive buffer: " + packet);
    		switch(packet.flag){
    		case NachosPacket.STP:
    			switch(state){
    			case ESTABLISHED:
    				state = STP_RCVD;
    				continue;
    			case STP_SENT:
    				sendFin();
    				state = CLOSING;
    				continue;
    			case SYN_SENT:
    				sendSyn();
    				continue;
    			case CLOSING:
    				sendFin();
    				state=CLOSED;
    				continue;
    			}
    		    		   				
    		case 0:  //data packet
    			switch(state){
    			case STP_SENT:
    				sendSTP();
    				continue;
    			case CLOSING:
    				sendFin();
    				continue;
    			case SYN_SENT:
    				sendSyn();
    			case ESTABLISHED:
    			case STP_RCVD:
    				sendAck(packet);
	        		int bytesToRead = (length > packet.contents.length) ? packet.contents.length : length;
	        		System.arraycopy(packet.contents, 0, buffer, offset,bytesToRead);
	        		if(receiveBuffer.size() == 0 && bytesToRead < packet.contents.length){
	        			//it is the last packet, there may be some bytes left over, 
	        			// store them in the readBuffer:
	        			System.arraycopy(packet.contents, bytesToRead, 
	        					readBuffer, 0, packet.contents.length - bytesToRead);
	        			readBufferHead = 0;
	        			readBufferTail = packet.contents.length - bytesToRead;
	        		}
	        		bytesRead = bytesRead + bytesToRead;
	        		length = length - bytesToRead;
	        		offset = offset + bytesToRead;
	        		break;
    			
    			}
        	}
        	
      	}
    	receiveBufferLock.release();
    	
       	return bytesRead;
    	
    }
    
    public int write(byte[] buffer, int offset, int length ){
    	//check for connection state before writing
    	if(this.state != ESTABLISHED)
    		return -1;
    	Lib.debug(dbgNet, "Starting socket write on socket: " + srcPort);
    	int bytesWritten = 0;
    	byte[] msgContent =null;
    	NachosPacket packet = null;
    	
    	while(length > 0){
    		if(length > NachosPacket.maxContentsLength)
    			msgContent = new byte[NachosPacket.maxContentsLength];
    		else
    			msgContent = new byte[length];
    		System.arraycopy(buffer, bytesWritten, msgContent, 0, msgContent.length);
    		try {
				packet = new NachosPacket(this.dstHostID, this.dstPort,
								Machine.networkLink().getLinkAddress(), this.srcPort,
								msgContent, getNextSeqNo());
				
			} catch (MalformedPacketException e) {
				Lib.debug(dbgNet, "Caught MalformedPacketException");
				return -1;
			}
				int packetsInBuffer = putInSendBuffer(packet);
				//if there are less than windowSize packets in
				// the buffer, then we can send this packet. 
				//Otherwise the retransmitter thread will pick it 
				// up from the buffer
				if( packetsInBuffer < windowSize){
					protocol.send(packet);
				}
				offset = offset + msgContent.length;
				length = length - msgContent.length;
				bytesWritten = bytesWritten + msgContent.length;
    	}
    	
    	return bytesWritten;
    }
    
    /**
     * Adds packet to the end of the send buffer and returns the size of the buffer
     * We need to synchronize access to this buffer, since the retransmitter 
     * thread will be accessing it too. 
     * will be accessing it too. 
     * 
     * @param packet
     * @return current buffer size
     */
    public int putInSendBuffer(NachosPacket packet){
    	int index = -1;
    	this.sendBufferLock.acquire();
    	sendBuffer.add(packet);
    	index = sendBuffer.size();
    	this.sendBufferLock.release();
    	return index;
    }
    
    /**
     * Adds packet to the end of the receive buffer and returns the size of the buffer
     * We need to synchronize access to this buffer, since the protocol 
     * thread will be accessing it too. 
     * will be accessing it too. 
     * 
     * @param packet
     * @return current buffer size
     */
    public int putInReceiveBuffer(NachosPacket packet){
    	int index = -1;
    	this.receiveBufferLock.acquire();
    	Lib.debug(dbgNet, "Received packet: " + packet.seqNo + ", expected packet: " + nextToReceive);
    	if(packet.seqNo == nextToReceive){
    		receiveBuffer.add(packet);
    		switch(packet.flag){
    		case NachosPacket.DATA:
    		case NachosPacket.SYN:
    		case NachosPacket.STP:
    		case NachosPacket.FIN:
    			setNextToReceive();
    			break;
    		}
    		
    	}
    	index = receiveBuffer.size();
    	this.receiveBufferLock.release();
    	return index;
    }
    
    private int getNextSeqNo(){
    	if(nextSeqNo < 32)
    		nextSeqNo++;
    	else{
    		nextSeqNo = 0;
    	}
    	return nextSeqNo;
    }
    
    private void setNextToReceive(){
    	if(nextToReceive < 32)
    		nextToReceive++;
    	else{
    		nextToReceive = 0;
    	}
    	
    }
    
  
    /**
     * Check just the first packet in the window, the receiver is not accepting packets
     * out of order. 
     * @param seqNo
     */
    public void handleAck(int seqNo){
    	boolean empty = false;
       	sendBufferLock.acquire();
    	if(sendBuffer.size() > 0 && sendBuffer.get(0).seqNo == seqNo){
    		sendBuffer.remove(0);
    		if(sendBuffer.size() == 0)
    			empty = true;
    	}
    	sendBufferLock.release();
    	if(state == STP_SENT && empty){
    		sendFin();
    		this.state = CLOSING;
    	}
	}
    
    public void handleFin(int seqNo){
    	receiveBufferLock.acquire();
    	if(receiveBuffer.size() > 0 && receiveBuffer.get(0).seqNo == seqNo){
    		receiveBuffer.remove(0);
    	}
    	receiveBufferLock.release();
    	switch(state){
		case SYN_SENT:
			sendSyn();
			break;
		case CLOSED:
		case ESTABLISHED:
		case STP_SENT:
		case STP_RCVD:
		case CLOSING:
			sendFinAck(seqNo);
			state = CLOSED;
			protocol.destroySocket(this);
			break;
		}
    	
    }
    
    public void handleSTP(int seqNo){
    	receiveBufferLock.acquire();
    	if(receiveBuffer.size() > 0 && receiveBuffer.get(0).seqNo == seqNo){
    		receiveBuffer.remove(0);
    	}
    	receiveBufferLock.release();
    	switch(state){
		case ESTABLISHED:
			state = STP_RCVD;
			break;
		case STP_SENT:
			sendFin();
			state = CLOSING;
			break;
		case SYN_SENT:
			sendSyn();
			break;
		case CLOSING:
			sendFin();
			state=CLOSED;
			break;
		}
    }
    
    /** 
     * Get the data packet from the receive queue and send the ack
     */
    public void handleData(NachosPacket packet){
    	Lib.debug(dbgNet, "Processing data packet: " + packet);
    	boolean sendAck = false;
    	receiveBufferLock.acquire();
    	if(receiveBuffer.size() != maxReceiveBufferSize){
    		//room in the buffer, take the packet
    		receiveBuffer.add(packet);
    		sendAck = true;
    	}
    	receiveBufferLock.release();
    	if(sendAck){
    		try {
				NachosPacket ack =  new NachosPacket(this.dstHostID, this.dstPort,
							Machine.networkLink().getLinkAddress(), this.srcPort,
							new byte[0], packet.seqNo, NachosPacket.ACK);
				this.protocol.send(ack);
			} catch (MalformedPacketException e) {
				Lib.debug(dbgNet, "MalformedPacketException while creating ACK for packet " +
								packet);
				e.printStackTrace();
			}
    	}
    }
    
    /**
     * Open socket. This will send the SYN packet to the client and block waiting for ACK
     * 
     */
    public boolean open(){
    	boolean retVal = true;
    	Lib.debug(dbgNet, "Opening a connection to " + dstHostID + ":" + dstPort);
    	sendSyn();
		waitForConnSignal.P();
		receiveBufferLock.acquire();
		//should really never happen, but check
		if(receiveBuffer.size() == 0)
			retVal = false;
		else{
			NachosPacket synack = receiveBuffer.remove(0);
			receiveBufferLock.release();
			//now remove the SYN packet from the buffer, since we go the ack
			sendBufferLock.acquire();
			sendBuffer.remove(0);
			sendBufferLock.release();
			this.state = ESTABLISHED;
		}
		
		return retVal;
		
    }
    
    public boolean acceptRequest(){
    	boolean retVal = true; // asume everything is OK
    	//Lib.debug(dbgNet, "Listening for connections on port: " + srcPort);
    	    	
    	receiveBufferLock.acquire();
		//should really never happen, but check
		if(receiveBuffer.size() == 0){
			retVal = false;
			receiveBufferLock.release();
		}else{
			//Get the SYN, send SYN_ACK, change the state to Established
			NachosPacket syn = receiveBuffer.remove(0);
			receiveBufferLock.release();
			sendSynAck(syn);
			this.state = ESTABLISHED;
		}
		return retVal;
    }
    
    /**
     * overriden close() method from the OpenFile class. It closes the socket gracefully
     * @param packet
     */
   public void close(){
	   Lib.debug(dbgNet, "Closing a connection to " + dstHostID + ":" + dstPort);
	   if(state == CLOSED)
		   return;
	  if(sendBuffer.size() == 0){
		   sendFin();
		   state = CLOSING;
	   }else{
		   sendSTP();
		   state = STP_SENT;
	   }
	   
   }
    
   private void sendAck(NachosPacket packet){
	   NachosPacket ack = null;
	   try {
			ack = new NachosPacket(this.dstHostID, this.dstPort,
							this.srcHostID, this.srcPort,
							new byte[0], packet.seqNo, NachosPacket.ACK);
			
		} catch (MalformedPacketException e) {
			Lib.debug(dbgNet, "Caught MalformedPacketException");
			//this really should not happen
		}  
		protocol.send(ack);
   }
   
   private void sendSynAck(NachosPacket packet){
	   NachosPacket synAck = null;
	   try {
			synAck = new NachosPacket(this.dstHostID, this.dstPort,
							this.srcHostID, this.srcPort,
							new byte[0], packet.seqNo, NachosPacket.SYN_ACK);
			
		} catch (MalformedPacketException e) {
			Lib.debug(dbgNet, "Caught MalformedPacketException");
			//this really should not happen
		}  
		protocol.send(synAck);
   }
   
   private void sendFin(){
	   NachosPacket fin = null;
	   try {
			fin = new NachosPacket(this.dstHostID, this.dstPort,
							Machine.networkLink().getLinkAddress(), this.srcPort,
							new byte[0], getNextSeqNo(), NachosPacket.FIN);
			
		} catch (MalformedPacketException e) {
			Lib.debug(dbgNet, "Caught MalformedPacketException");
			//this really should not happen
		}  
		
		int packetsInBuffer = putInSendBuffer(fin);
		if( packetsInBuffer < windowSize){
			protocol.send(fin);
		}
		
   }
   
   private void sendFinAck(int seqNb){
	   NachosPacket finAck = null;
	   try {
			finAck = new NachosPacket(this.dstHostID, this.dstPort,
							this.srcHostID, this.srcPort,
							new byte[0], seqNb, NachosPacket.FIN_ACK);
			
		} catch (MalformedPacketException e) {
			Lib.debug(dbgNet, "Caught MalformedPacketException");
			//this really should not happen
		}  
		int packetsInBuffer = putInSendBuffer(finAck);
		if( packetsInBuffer < windowSize){
			protocol.send(finAck);
		}
   }
   
   private void sendSTP(){
	   NachosPacket stp = null;
  		try {
			stp =  new NachosPacket(this.dstHostID, this.dstPort,
					Machine.networkLink().getLinkAddress(), this.srcPort,
					new byte[0], getNextSeqNo(), NachosPacket.STP);
		} catch (MalformedPacketException e) {
			// Should never get here....
			e.printStackTrace();
		}
		int packetsInBuffer = putInSendBuffer(stp);
		if( packetsInBuffer < windowSize){
			protocol.send(stp);
		}
   }
   
   private void sendSyn(){
	   NachosPacket syn = null;
   		try {
			syn =  new NachosPacket(this.dstHostID, this.dstPort,
					Machine.networkLink().getLinkAddress(), this.srcPort,
					new byte[0], getNextSeqNo(), NachosPacket.SYN);
		} catch (MalformedPacketException e) {
			// Should never get here....
			e.printStackTrace();
		}
		
		this.state = SYN_SENT;
		protocol.send(syn);
		putInSendBuffer(syn);
   }
  
}
